﻿using System;
using System.Collections.Generic;
using System.Linq;
using DiscreteSimulation.RandomGeneration;
using DiscreteSimulation.Util;

namespace DiscreteSimulation.Simulation
{
    /// <summary>
    /// This discrete simulator is intended to simulate
    /// one first queue where all the clients arrive and
    /// two second queues where the clients that leave
    /// the first queue will go inside to.
    /// clients can leave away from any queue.
    /// </summary>
    public class DiscreteSimulator
    {
        #region Constants
        private const string mainQueueId = "MainQueue";
        private const string subQueueId = "SubQueue";
        #endregion

        #region Properties
        private int Iterations
        {
            get;
            set;
        }
        private LCGPseudoRandomGenerator Generator
        {
            get;
            set;
        }

        private double SimulationTime
        {
            get;
            set;
        }

        public Dictionary<string, SimulationQueue> Queues
        {
            get;
            set;
        }

        public Dictionary<double, LinkedList<FutureEvent>> FutureEvents
        {
            get;
            set;
        }

        public Dictionary<string, Dictionary<int, double>> Counter
        {
            get;
            set;
        }
        #endregion

        public DiscreteSimulator(int iterations, LCGPseudoRandomGenerator generator)
        {
            // Setting number of simulation iterations
            Iterations = iterations;
            Generator = generator;
            SimulationTime = 0.0;

            // Initialization
            FutureEvents = new Dictionary<double, LinkedList<FutureEvent>>();
            Queues = new Dictionary<string, SimulationQueue>();
            Counter = new Dictionary<string, Dictionary<int, double>>();
            
            // Adding queues
            Queues.Add(mainQueueId, new SimulationQueue(2, 1, 0.1, 0.5));
            Counter.Add(mainQueueId, new Dictionary<int, double>());
            for (int i = 0; i <= Queues[mainQueueId].Capacity; i++)
            {
                Counter[mainQueueId][i] = 0.0;
            }

            Queues.Add(subQueueId, new SimulationQueue(2, 2, 0.2, 0.6));
            Counter.Add(subQueueId, new Dictionary<int, double>());
            for (int i = 0; i <= Queues[subQueueId].Capacity; i++)
            {
                Counter[subQueueId][i] = 0.0;
            }
        }

        public void Simulate()
        {
            // Adding first event
            double time = Generator.GenerateExponencialPseudoRandomWithAverage(Queues[mainQueueId].AverageOrPercentage);
            AddEvent(time, EventType.MainQueueAdd);

            for (int i = 0; i < Iterations; i++)
            {
                // Get next event on events queue.
                FutureEvent futureEvent = NextEvent();

                double simTime = SimulationTime - futureEvent.Time <= 0 ? futureEvent.Time : SimulationTime - futureEvent.Time;
                SimulationTime += simTime;
                Counter[mainQueueId][Queues[mainQueueId].Clients] += simTime;
                Counter[subQueueId][Queues[subQueueId].Clients] += simTime;

                switch (futureEvent.Type)
                {
                    case EventType.MainQueueAdd:
                        ProcessMainQueueAdd(futureEvent);
                        break;
                    case EventType.MainQueueRemove:
                        ProcessMainQueueRemove(futureEvent);
                        break;
                    case EventType.SubQueueAdd:
                        ProcessSubQueueAdd(futureEvent);
                        break;
                    case EventType.SubQueueRemove:
                        ProcessSubQueueRemove(futureEvent);
                        break;
                    default:
                        throw new Exception("Cannot Handle Unexpected EventType.");
                }
            }
        }

        private void ProcessMainQueueAdd(FutureEvent futureEvent)
        {
            ProcessSubQueueRemove(futureEvent);

            if (!AddRemoveClient(futureEvent, mainQueueId, true))
            {
                Console.WriteLine("Got exception... Client lost!");
                return;
            }

            if (Queues[mainQueueId].Clients <= Queues[mainQueueId].Servers)
            {
                MainQueueScheduling();
            }

            double newTime = Generator.GenerateExponencialPseudoRandomWithAverage(Queues[mainQueueId].AverageOrPercentage);
            AddEvent(SimulationTime + newTime, EventType.MainQueueAdd);
        }

        private void ProcessMainQueueRemove(FutureEvent futureEvent)
        {
            AddRemoveClient(futureEvent, mainQueueId, false);

            if (Queues[mainQueueId].Clients >= Queues[mainQueueId].Servers)
            {
                MainQueueScheduling();
            }
        }

        private void ProcessSubQueueAdd(FutureEvent futureEvent)
        {
            ProcessMainQueueRemove(futureEvent);

            if(!AddRemoveClient(futureEvent, subQueueId, true))
            {
                Console.WriteLine("Got exception... Client lost!");
                return;
            }

            if (Queues[subQueueId].Clients <= Queues[subQueueId].Servers)
            {
                SubQueueScheduling();
            }

            double newTime = Generator.GenerateExponencialPseudoRandomWithAverage(Queues[mainQueueId].Service);
            AddEvent(SimulationTime + newTime, EventType.SubQueueAdd);
        }

        private void ProcessSubQueueRemove(FutureEvent futureEvent)
        {
            AddRemoveClient(futureEvent, subQueueId, false);

            if (Queues[subQueueId].Clients >= Queues[subQueueId].Servers)
            {
                SubQueueScheduling();
            }
        }

        private void MainQueueScheduling()
        {
            double percentage = Queues[subQueueId].AverageOrPercentage;
            double generated = Generator.GenerateUniformPseudoRandomWithoutModulus();
            double time = Generator.GenerateExponencialPseudoRandomWithAverage(Queues[mainQueueId].Service);
            if (generated <= percentage)
            {
                AddEvent(SimulationTime + time, EventType.SubQueueAdd);
            }
            else
            {
                AddEvent(SimulationTime + time, EventType.MainQueueRemove);
            }
        }

        private void SubQueueScheduling()
        {
            double percentage = Queues[mainQueueId].AverageOrPercentage;
            double generated = Generator.GenerateUniformPseudoRandomWithoutModulus();
            double time = Generator.GenerateExponencialPseudoRandomWithAverage(Queues[subQueueId].Service);
            if (generated <= percentage)
            {
                AddEvent(SimulationTime + time, EventType.MainQueueAdd);
            }
            else
            {
                AddEvent(SimulationTime + time, EventType.SubQueueRemove);
            }
        }

        private bool AddRemoveClient(FutureEvent futureEvent, string queueId, bool isAdd)
        {
            int oldVal = isAdd ? Queues[queueId].AddClient() : Queues[queueId].RemoveClient();
            if (oldVal == -1) return false;
            return true;
        }

        private FutureEvent NextEvent()
        {
            FutureEvent ret = FutureEvents[FutureEvents.Keys.Min()].First.Value;

            if (FutureEvents[FutureEvents.Keys.Min()].Count <= 1)
            {
                
                FutureEvents.Remove(FutureEvents.Keys.Min());
            }
            else
            {
                FutureEvents[FutureEvents.Keys.Min()].RemoveFirst();
            }
            
            return ret;
        }

        private void AddEvent(double time, EventType type)
        {
            if (FutureEvents.ContainsKey(time))
            {
                FutureEvents[time].AddLast(new FutureEvent(time, type));
            }
            else
            {
                LinkedList<FutureEvent> list = new LinkedList<FutureEvent>();
                list.AddLast(new FutureEvent(time, type));
                FutureEvents.Add(time, list);
            }
        }

        public void PrintStatistics()
        {
            Console.WriteLine();
            Console.WriteLine("Simulation Time: " + SimulationTime);
            foreach (string queue in Counter.Keys)
            {
                Console.WriteLine();
                Console.WriteLine(queue);
                foreach (int len in Counter[queue].Keys)
                {
                    Console.WriteLine();
                    Console.WriteLine(len + ": " + Counter[queue][len]);
                    Console.WriteLine("%: " + (Counter[queue][len] * 100) / SimulationTime);
                }
            }
        }
    }
}
